// locutor
// (c) 2008-2009, Normen Müller, normen.mueller@gmail.com  
// $Id: SVN.scala 2451 2009-10-01 14:04:14Z nmueller $
package locutor.svn

import java.io.File
import java.net.URI
import java.util.regex.Pattern._
import java.text.MessageFormat.{format}

import scaup.Views._
import scaup.io.FileType._

import locutor.Views._

/** The object <code>SVN</code> provides Subversion utility methods.
 * 
 *  <p><strong>Note:</strong> As <a href="http://svnkit.com/">SVNKit</a> also has to parse externals definitions
 *  located in respective <code>dir-props-base</code> files, but within Scala this is done much faster, we
 *  re-implemented the retrieval of externals definitions here.  However, we only consider <code>dir-props-base</code>
 *  files, rather than <code>dir-props</code> files in addition.  The latter store not yet committed Subversion
 *  directory properties.</p>
 * 
 *  <p>@thanks to Heinrich Stamerjohanns &lt;h.stamerjohanns@jacobs-university.de&gt; for his help on the internal
 *  regular exression to extract <code>svn:externals</code> values out of <code>dir-props-base</code> files.
 *  Formerly solved by parser combinators.</p>
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
object SVN {

  private val parse = EXTDEF.parse _ //ExtDefParser.parse _
  
  private val matcher = """(svn:externals).(V\s\d+).(.*?).(END|K )""" regex DOTALL|MULTILINE

  /** Returns a mapping from file paths to externals definitions on the versioned file system tree
   *  rooted at <code>root</code>.
   * 
   * <p><strong>Note:</strong>
   * <ul>
   *  <li>Peg revision are removed.</li>
   *  <li>Cascading externals are omitted</li>
   *  <li>Transformed externals are included</li>
   * </p>
   * 
   * @param  root path of versioned file system tree to retrieve externals definitions for
   * @return      a mapping from file paths to externals definitions on the versioned file system tree
   *              rooted at <code>root</code>. */
  def externalsOf(root: File) = extsOf(root,Set.empty)
  
  private def extsOf(f: File,s: Set[File]): List[EXT] =
    if(s contains f)
      Nil
    else extsOf(f.ls filter (f => f.isDirectory && !(f.p endsWith ".svn")),s + f,extsDefsOf(f) map {
      case e@ExtDef(r,u,_,t) => EXT(f,r,e.resolve(f.vc.repos.get,f.vc.url.get),t,Nil)
    })
  
  private def extsOf(fs: List[File],s: Set[File],es: List[EXT]): List[EXT] =
    es ::: (fs flatMap (f => if(es exists (e => f <= e.own/e.tgt)) Nil else extsOf(f,s)))
  
  def externalsDefinitionsOf(f: File) = extsDefsOf(f)
  
  private def extsDefsOf(f: File) =
    if(f/".svn/dir-prop-base" exists)
      (matcher findAllIn (f/".svn/dir-prop-base").content flatMap {
        case matcher(_,_,s,_) => s.lines filterMapOption parse
        case _                => Iterator.empty
      }).toList
    else Nil
  
  def unlock(d: Option[File]): Unit = d match {
    case Some(d) => unlock(d)
    case _       => ()
  }
  
  def unlock(d: File): Unit =
    for(f <- d.ls if f.t == DIRECTORY)
      if (f.getName == ".svn")
        (f/"lock").remove()
      else unlock(f)  
}

case class SVNError(message: String) extends Throwable(message)

/**
 * @todo document
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
class SVN @throws(classOf[SVNError]) (file: File) {
  import SVN._
  
  if (file == null)
    throw SVNError("svn: File may not be null")
  if (!file.exists)
    throw SVNError("svn: " + file.path + " does not exist")
  
  private lazy val entries = (file / ".svn/entries").content
  
  /** @todo document */
  lazy val isExternal: Boolean = 
    if((file/".svn") exists)
      url match {
        case Some(fu) => file.pf.vc.url match {
          case Some(pu) => !(fu < pu)
          case None     => false
        }
        case None     => false
      }
    else false
  
  /** @todo document */
  lazy val uuid = if((file/".svn") exists) Some(entries.lines.slice(26,27) mkString) else None
  
  /** @todo implement */
  lazy val repos = if((file/".svn") exists) Some(new java.net.URI(entries.lines.slice(5,6) mkString)) else None
  
  /** @todo document */
  lazy val url = if((file/".svn") exists) Some(new java.net.URI(entries.lines.slice(4,5) mkString)) else None
  
  /** @todo document */
  def root(stopOnExternals: Boolean): Option[File] =
    if((file/".svn") exists)
      if(file.pf != null)
        if(isRoot)
          if(! stopOnExternals && isExternal)
            file.pf.vc.root(stopOnExternals) 
          else Some(file)
        else file.pf.vc.root(stopOnExternals)
      else Some(file)
    else None
  
  /** @todo document */
  lazy val isRoot: Boolean = 
    if(file.pf != null || ! ((file/".svn") exists))
      if((file.pf/".svn") exists)
        (url,file.pf.vc.url) match {
          case (Some(fu),Some(pu)) if ! (fu < pu) => true
          case (Some(_),Some(_))                  => false
          case (_,_) => throw new Error(format("''{0}'' has no ancestry information",Array[AnyRef](file.pp): _*))
        }        
      else true
    else false  
}

/** Version controlled directory
 * 
 *  Note: WC, EXT, and TEX all directly extend VC due to https://lampsvn.epfl.ch/trac/scala/ticket/425
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
sealed abstract class VC(val rev: Long,val uri: URI,val dir: File,val es: List[VC]) {
  def this(rev: Long,uri: URI,dir: File) = this(rev,uri,dir,Nil)
  
  val id: String = dir.p.toMD5
  
  // Note: ``isAdequateTo'' is a directed relation, emphasized by ``~>''
  def ~>(n: VC): Boolean = isAdequateTo(n)
  
  def isAdequateTo(n: VC): Boolean
  
  def toXML: scala.xml.Node
}

/** Working copy
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
case class WC(override val uri: URI,override val dir: File,override val es: List[VC]) extends VC(-1,uri,dir,es) {
  def isAdequateTo(n: VC): Boolean = id != n.id && n.rev == -1 && n.uri <= uri
  
  def toXML = (<wc xml:id={id} url={uri.toString} root={dir.p}>{es map (_.toXML)}</wc>)
  
  override def toString = "WC("+id+","+uri+","+dir.p+","+es+")"
}

/** Definitional VC items
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
trait Definitional {
  val own: File
  
  val tgt: String  
}

/** External
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
case class EXT(val own: File,override val rev: Long,override val uri: URI,val tgt: String,override val es: List[VC])
  extends VC(rev,uri,own/tgt,es) with Definitional {
    
    override def isAdequateTo(n: VC) = id != n.id && n.rev == rev && n.uri <= uri
  
    override def toXML =
      (<external xml:id={id} own={own.p} rev={rev.toString} url={uri.toString} tgt={tgt}>{es map (_.toXML)}</external>)
  
    override def toString = "EXT("+id+","+rev+","+uri+","+dir.p+","+es+")"
}

/** Transformed external
 * 
 * @author <a href="mailto:normen.mueller@googlemail.com">Normen M&#xFC;ller</a>
 * @version 0.0.1 */
case class TEX(val own: File,override val rev: Long,override val uri: URI,val tgt: String,val ref: String)
  extends VC(rev,uri,own/tgt) with Definitional {
    
    override def isAdequateTo(n: VC) = false
  
    override def toXML =
      (<external xml:id={id} own={own.p} rev={rev.toString} url={uri.toString} tgt={tgt} xref={"#"+ref}/>)
  
    override def toString = "TEX("+id+","+rev+","+uri+","+dir.p+","+ref+")"
}
